import { Helper } from "../core/helper";
import { types } from "../core/types";
import { ulong_buf_reverse } from "../core/ulong_buf_reverse";

const Rp = [
  0, 1, 190, 28, 91, 36, 300, 6, 55, 276, 3, 10, 171, 153, 231, 105, 45, 15, 21, 136, 210, 66, 253, 120, 78
];
const RC: bigint[] = [
  types.ulong(0x01),
  types.ulong(0x8082),
  types.ulong(0x0000808a, 0x80000000),
  types.ulong(0x80008000, 0x80000000),
  types.ulong(0x808b),
  types.ulong(0x80000001),
  types.ulong(0x80008081, 0x80000000),
  types.ulong(0x00008009, 0x80000000),
  types.ulong(0x8a),
  types.ulong(0x88),
  types.ulong(0x80008009),
  types.ulong(0x8000000a),
  types.ulong(0x8000808b),
  types.ulong(0x0000008b, 0x80000000),
  types.ulong(0x00008089, 0x80000000),
  types.ulong(0x00008003, 0x80000000),
  types.ulong(0x00008002, 0x80000000),
  types.ulong(0x00000080, 0x80000000),
  types.ulong(0x800a),
  types.ulong(0x8000000a, 0x80000000),
  types.ulong(0x80008081, 0x80000000),
  types.ulong(0x00008080, 0x80000000),
  types.ulong(0x80000001),
  types.ulong(0x80008008, 0x80000000)
]
class SHA3_CTX {
  public kind: number;
  public shake: boolean;
  public keccak: boolean;
  public lane: bigint[][];
  public b: number;
  public r: number;
  public c: number;
  public nr: number;
  public md_size: number;
  public absorbing: boolean;
  public get buf(): bigint[] {
    return this.buffer.Oo.Data;
  }
  public buffer: ulong_buf_reverse;
}
export class SHA3 {
  constructor() {

  }
  public static ROTL(x: bigint, shift: number): bigint {
    return Helper.ulong((x << BigInt(shift))) | (x >> BigInt(64 - shift));
  }
  public static theta(ctx: SHA3_CTX) {
    let x, y;
    let C: bigint[], D: bigint[];
    C = new Array(5);
    D = new Array(5);
    for (x = 0; x < 5; x++)
      C[x] = ctx.lane[0][x] ^ ctx.lane[1][x] ^ ctx.lane[2][x] ^ ctx.lane[3][x] ^ ctx.lane[4][x];
    for (x = 0; x < 5; x++)
      D[x] = C[(x + 4) % 5] ^ this.ROTL(C[(x + 1) % 5], 1);
    for (y = 0; y < 5; y++)
      for (x = 0; x < 5; x++)
        ctx.lane[y][x] ^= D[x];
  }
  public static rho(ctx: SHA3_CTX) {
    let x, y, m;
    let t;

    x = 1;
    y = 0;
    for (t = 0; t < 24; t++) {
      ctx.lane[y][x] = SHA3.ROTL(ctx.lane[y][x], (Rp[y * 5 + x] % 64));
      m = x;
      x = y;
      y = (2 * m + 3 * y) % 5;
    }
  }
  public static pi(ctx: SHA3_CTX) {
    let Ap: bigint[][] = new Array(5);
    let x, y;

    for (y = 0; y < 5; y++) {
      Ap[y] = new Array(5);
      for (x = 0; x < 5; x++) {
        Ap[y][x] = ctx.lane[x][(x + 3 * y) % 5];
      }
    }
    Helper.ArrayCopy(Ap, ctx.lane, 5);
  }
  public static chi(ctx: SHA3_CTX) {
    let Ap: bigint[][] = new Array(5);
    let x, y;

    for (y = 0; y < 5; y++) {
      Ap[y] = new Array(5);
      for (x = 0; x < 5; x++) {
        Ap[y][x] = ctx.lane[y][x] ^ ((~ctx.lane[y][(x + 1) % 5]) & ctx.lane[y][(x + 2) % 5]);
      }
    }

    Helper.ArrayCopy(Ap, ctx.lane, 5);
  }
  public static iota(ctx: SHA3_CTX, i: number) {
    ctx.lane[0][0] ^= RC[i];
  }
  public static ProcessBlock(ctx: SHA3_CTX) {
    let t;

    if (ctx.absorbing) {
      let i: number;
      for (i = 0; i < ((ctx.b / 8) >> 0); i++) {
        if (i < ((ctx.r / 8) >> 0)) {
          ctx.lane[(i / 5) >> 0][i % 5] ^= ctx.buf[i];
        }
        else {
          ctx.lane[(i / 5) >> 0][i % 5] ^= BigInt(0x0);
        }
      }
    }
    for (t = 0; t < ctx.nr; t++) {
      SHA3.theta(ctx);
      SHA3.rho(ctx);
      SHA3.pi(ctx);
      SHA3.chi(ctx);
      SHA3.iota(ctx, t);
    }
  }
  public static Init(kind: number, shake: boolean, d: number): SHA3_CTX {
    let ctx = new SHA3_CTX();
    ctx.buffer = ulong_buf_reverse.New(25);
    ctx.b = 200;
    ctx.kind = kind;
    ctx.shake = shake;

    if (ctx.shake) {
      if (ctx.kind == 128) {
        if (d == 0) d = 128;
        ctx.r = 168;
        ctx.c = 32;
        ctx.md_size = (d / 8) >> 0;
      }
      else {
        if (d == 0) d = 256;
        ctx.r = 136;
        ctx.c = 64;
        ctx.md_size = (d / 8) >> 0;
      }
    }
    else {
      switch (kind) {
        case 224:
          ctx.r = 144;
          ctx.c = 56;
          ctx.md_size = 28;
          break;
        case 256:
          ctx.r = 136;
          ctx.c = 64;
          ctx.md_size = 32;
          break;
        case 384:
          ctx.r = 104;
          ctx.c = 96;
          ctx.md_size = 48;
          break;
        default:
        case 512:
          ctx.r = 72;
          ctx.c = 128;
          ctx.md_size = 64;
          break;
      }
    }
    ctx.nr = 24;
    ctx.absorbing = true;
    ctx.lane = new Array(5);
    for (let i = 0; i < 5; i++) {
      ctx.lane[i] = new Array(5);
      for (let j = 0; j < 5; j++) ctx.lane[i][j] = BigInt(0);
    }
    return ctx;
  }
  public static Update(ctx: SHA3_CTX, data: number[], start: number, length: number) {
    for (let i of ctx.buffer.Push(data, start, length, (200 - ctx.r) >> 3)) {
      if (i <= length) {
        SHA3.ProcessBlock(ctx);
      }
    }
  }
  public static Final(ctx: SHA3_CTX): number[] {
    let i = ctx.buffer.UsedBytes;
    if (i <= ctx.r - 2) {
      if (i % 8 != 0) {
        ctx.buffer.ToNext();
      }
      ctx.buffer.Fill(0, (200 - ctx.r) >> 3);
      if (ctx.shake) {
        ctx.buffer.SetByte(0x1F, i);
        ctx.buffer.SetByte(0x80, (ctx.r - 1));
      }
      else if (ctx.keccak) {
        ctx.buffer.SetByte(0x01, i);
        ctx.buffer.SetByte(0x80, (ctx.r - 1));
      }
      else {
        ctx.buffer.SetByte(0x06, i);
        ctx.buffer.SetByte(0x80, (ctx.r - 1));
      }
    }
    else {
      if (ctx.shake) {
        ctx.buffer.SetByte(0x9f, i);
      }
      else if (ctx.keccak) {
        ctx.buffer.SetByte(0x81, i);
      }
      else {
        ctx.buffer.SetByte(0x86, i);
      }
    }

    SHA3.ProcessBlock(ctx);
    ctx.absorbing = false;
    var sha = new Array(ctx.md_size);
    if (ctx.md_size <= ctx.r) {
      Helper.CopyFrom88(sha, 0, ctx.lane, 0, ctx.md_size, 5);
    }
    else {
      Helper.CopyFrom88(sha, 0, ctx.lane, 0, ctx.r, 5);
      var md_size = ctx.r;

      while (md_size < ctx.md_size) {
        SHA3.ProcessBlock(ctx);
        if (ctx.md_size - md_size > ctx.r) {
          Helper.CopyFrom88(sha, md_size >>> 3, ctx.lane, 0, ctx.r, 5);
          md_size += ctx.r;
        }
        else {
          Helper.CopyFrom88(sha, md_size >>> 3, ctx.lane, 0, ctx.md_size - md_size, 5);
          md_size = ctx.md_size;
        }
      }
    }
    return sha;
  }
}

export class SHA3_512 {
  public constructor() {
  }
  public _ctx: SHA3_CTX;
  public New() {
    this._ctx = SHA3.Init(512, false, 0);
  }
  public Update(data: number[]) {
    SHA3.Update(this._ctx, data, 0, data.length);
  }
  public Make(): string {
    return Helper.ToHex(SHA3.Final(this._ctx));
  }
  public MakeBytes() {
    return SHA3.Final(this._ctx);
  }
  public static JustMakeBytes(data: number[]): number[] {
    var ctx = SHA3.Init(512, false, 0);
    SHA3.Update(ctx, data, 0, data.length);
    return SHA3.Final(ctx);
  }
  public static JustMake(data: number[]): string {
    return Helper.ToHex(this.JustMakeBytes(data));
  }
}
export class SHA3_224 {
  public constructor() {
  }
  public _ctx: SHA3_CTX;
  public New() {
    this._ctx = SHA3.Init(224, false, 0);
  }
  public Update(data: number[]) {
    SHA3.Update(this._ctx, data, 0, data.length);
  }
  public Make(): string {
    return Helper.ToHex(SHA3.Final(this._ctx));
  }
  public MakeBytes() {
    return SHA3.Final(this._ctx);
  }
  public static JustMakeBytes(data: number[]): number[] {
    var ctx = SHA3.Init(224, false, 0);
    SHA3.Update(ctx, data, 0, data.length);
    return SHA3.Final(ctx);
  }
  public static JustMake(data: number[]): string {
    return Helper.ToHex(this.JustMakeBytes(data));
  }
}
export class SHA3_256 {
  public constructor() {
  }
  public _ctx: SHA3_CTX;
  public New() {
    this._ctx = SHA3.Init(256, false, 0);
  }
  public Update(data: number[]) {
    SHA3.Update(this._ctx, data, 0, data.length);
  }
  public Make(): string {
    return Helper.ToHex(SHA3.Final(this._ctx));
  }
  public MakeBytes() {
    return SHA3.Final(this._ctx);
  }
  public static JustMakeBytes(data: number[]): number[] {
    var ctx = SHA3.Init(256, false, 0);
    SHA3.Update(ctx, data, 0, data.length);
    return SHA3.Final(ctx);
  }
  public static JustMake(data: number[]): string {
    return Helper.ToHex(this.JustMakeBytes(data));
  }
}
export class SHA3_384 {
  public constructor() {
  }
  public _ctx: SHA3_CTX;
  public New() {
    this._ctx = SHA3.Init(384, false, 0);
  }
  public Update(data: number[]) {
    SHA3.Update(this._ctx, data, 0, data.length);
  }
  public Make(): string {
    return Helper.ToHex(SHA3.Final(this._ctx));
  }
  public MakeBytes() {
    return SHA3.Final(this._ctx);
  }
  public static JustMakeBytes(data: number[]): number[] {
    var ctx = SHA3.Init(384, false, 0);
    SHA3.Update(ctx, data, 0, data.length);
    return SHA3.Final(ctx);
  }
  public static JustMake(data: number[]): string {
    return Helper.ToHex(this.JustMakeBytes(data));
  }
}
export class SHA3_SHAKE128 {
  public constructor() {
  }
  public _ctx: SHA3_CTX;
  public New() {
    this._ctx = SHA3.Init(128, true, 0);
  }
  public Update(data: number[]) {
    SHA3.Update(this._ctx, data, 0, data.length);
  }
  public Make(): string {
    return Helper.ToHex(SHA3.Final(this._ctx));
  }
  public MakeBytes() {
    return SHA3.Final(this._ctx);
  }
  public static JustMakeBytes(data: number[]): number[] {
    var ctx = SHA3.Init(128, true, 0);
    SHA3.Update(ctx, data, 0, data.length);
    return SHA3.Final(ctx);
  }
  public static JustMake(data: number[]): string {
    return Helper.ToHex(this.JustMakeBytes(data));
  }
}
export class SHA3_SHAKE256 {
  public constructor() {
  }
  public _ctx: SHA3_CTX;
  public New() {
    this._ctx = SHA3.Init(256, true, 0);
  }
  public Update(data: number[]) {
    SHA3.Update(this._ctx, data, 0, data.length);
  }
  public Make(): string {
    return Helper.ToHex(SHA3.Final(this._ctx));
  }
  public MakeBytes() {
    return SHA3.Final(this._ctx);
  }
  public static JustMakeBytes(data: number[]): number[] {
    var ctx = SHA3.Init(256, true, 0);
    SHA3.Update(ctx, data, 0, data.length);
    return SHA3.Final(ctx);
  }
  public static JustMake(data: number[]): string {
    return Helper.ToHex(this.JustMakeBytes(data));
  }
}
export class KECCAK_224 {
  public constructor() {
  }
  public _ctx: SHA3_CTX;
  public New() {
    this._ctx = SHA3.Init(224, false, 0);
    this._ctx.keccak = true;
  }
  public Update(data: number[]) {
    SHA3.Update(this._ctx, data, 0, data.length);
  }
  public Make(): string {
    return Helper.ToHex(SHA3.Final(this._ctx));
  }
  public MakeBytes() {
    return SHA3.Final(this._ctx);
  }
  public static JustMakeBytes(data: number[]): number[] {
    var ctx = SHA3.Init(224, false, 0);
    ctx.keccak = true;
    SHA3.Update(ctx, data, 0, data.length);
    return SHA3.Final(ctx);
  }
  public static JustMake(data: number[]): string {
    return Helper.ToHex(this.JustMakeBytes(data));
  }
}
export class KECCAK_256 {
  public constructor() {
  }
  public _ctx: SHA3_CTX;
  public New() {
    this._ctx = SHA3.Init(256, false, 0);
    this._ctx.keccak = true;
  }
  public Update(data: number[]) {
    SHA3.Update(this._ctx, data, 0, data.length);
  }
  public Make(): string {
    return Helper.ToHex(SHA3.Final(this._ctx));
  }
  public MakeBytes() {
    return SHA3.Final(this._ctx);
  }
  public static JustMakeBytes(data: number[]): number[] {
    var ctx = SHA3.Init(256, false, 0);
    ctx.keccak = true;
    SHA3.Update(ctx, data, 0, data.length);
    return SHA3.Final(ctx);
  }
  public static JustMake(data: number[]): string {
    return Helper.ToHex(this.JustMakeBytes(data));
  }
}
export class KECCAK_384 {
  public constructor() {
  }
  public _ctx: SHA3_CTX;
  public New() {
    this._ctx = SHA3.Init(384, false, 0);
    this._ctx.keccak = true;
  }
  public Update(data: number[]) {
    SHA3.Update(this._ctx, data, 0, data.length);
  }
  public Make(): string {
    return Helper.ToHex(SHA3.Final(this._ctx));
  }
  public MakeBytes() {
    return SHA3.Final(this._ctx);
  }
  public static JustMakeBytes(data: number[]): number[] {
    var ctx = SHA3.Init(384, false, 0);
    ctx.keccak = true;
    SHA3.Update(ctx, data, 0, data.length);
    return SHA3.Final(ctx);
  }
  public static JustMake(data: number[]): string {
    return Helper.ToHex(this.JustMakeBytes(data));
  }
}
export class KECCAK_512 {
  public constructor() {
  }
  public _ctx: SHA3_CTX;
  public New() {
    this._ctx = SHA3.Init(512, false, 0);
    this._ctx.keccak = true;
  }
  public Update(data: number[]) {
    SHA3.Update(this._ctx, data, 0, data.length);
  }
  public Make(): string {
    return Helper.ToHex(SHA3.Final(this._ctx));
  }
  public MakeBytes() {
    return SHA3.Final(this._ctx);
  }
  public static JustMakeBytes(data: number[]): number[] {
    var ctx = SHA3.Init(512, false, 0);
    ctx.keccak = true;
    SHA3.Update(ctx, data, 0, data.length);
    return SHA3.Final(ctx);
  }
  public static JustMake(data: number[]): string {
    return Helper.ToHex(this.JustMakeBytes(data));
  }
}